Skip to content

livekit_bridge: An ergonomic interface into the Livekit C++ SDK#58

Open
stephen-derosa wants to merge 9 commits intolivekit:mainfrom
stephen-derosa:feat/livekit_cpp_bridge.sdd
Open

livekit_bridge: An ergonomic interface into the Livekit C++ SDK#58
stephen-derosa wants to merge 9 commits intolivekit:mainfrom
stephen-derosa:feat/livekit_cpp_bridge.sdd

Conversation

@stephen-derosa
Copy link

@stephen-derosa stephen-derosa commented Feb 13, 2026

Overview

An ergonomic library providing simple usage of the C++ SDK.

Building

This library is attached to the build system of the core C++ SDK library. Use build.sh as is.

Testing

The bridge/examples/ directory has simulated human and robot tests. There are 4 files:

  • robot.cpp: publishes video and audio from a webcam and microphone. This requires a webcam and microphone to be available.
  • robot_stub.cpp: publishes stubbed audio and video. This exists to exemplify simplicity.
  • human.cpp: receives and renders video to the screen, receives and plays audio through the speaker.
  • human_stub.cpp: receives video and audio and prints that it was received. This exists to exemplify simplicity.

These have been tested manually.

Unit tests

  1. CallbackKey hashing/equality,
  2. BridgeAudioTrack/BridgeVideoTrack state management, and
  3. LiveKitBridge pre-connection behaviour (callback registration, error handling).

Limitations

The bridge is designed for simplicity and currently only supports limited audio and video features. It does not expose:

  • E2EE configuration
  • RPC / data channels / data tracks
  • Simulcast tuning
  • Video format selection (RGBA is the default; no format option yet)
  • Custom RoomOptions or TrackPublishOptions

For advanced use cases, use the full client-sdk-cpp API directly, or expand the bridge to support your use case.

TODOs before merge

  • multiple video/audio streams
  • ❓ what other tests should we require here to ensure we have a good infrastructure moving forward?
  • ❓ Other general LiveKit reqs?

@stephen-derosa stephen-derosa self-assigned this Feb 13, 2026
@stephen-derosa stephen-derosa added the enhancement New feature or request label Feb 13, 2026
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Collaborator

@xianshijing-lk xianshijing-lk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work, that is a large PR, I will need more time to review it.

some initial questions to get a clearer picture on the design.


// Check for a new video frame
{
std::lock_guard<std::mutex> lock(g_latest_video.mutex);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we try avoiding locking while doing the heavy duty things ?

I think this can be achieved by having a local_pixel, and just swap the buffer, like
std::vectorstd::uint8_t local_pixels;
int fw = 0, fh = 0;
bool have_frame = false;
{
std::lock_guardstd::mutex lock(g_latest_video.mutex);
if (g_latest_video.dirty && g_latest_video.width > 0 && g_latest_video.height > 0) {
fw = g_latest_video.width;
fh = g_latest_video.height;
local_pixels.swap(g_latest_video.data); // avoid copy
g_latest_video.dirty = false;
have_frame = true;
}
}

and use the local_pixel and other local variables for rendering.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great point -- updated in the latest code!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I think it is not recommended to call Connect() while holding the std::lock_guardstd::mutex lock(mutex_);

maybe it is better to create a local room, like
auto room = std::make_uniquelivekit::Room();
set things up, then reassign it to room_

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated via constructing the room, then acquiring the lock and std:move-ing to the member var. Also added a connecting_ flag to guard against connect() being called impatiently/improperly by developers

* @param token Access token for authentication.
* @return true if connection succeeded.
*/
bool connect(const std::string &url, const std::string &token);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curiously, don't you need to expose the room option via the connect() function ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My experimentation shows me that you dont actually need to specify the room, which i think makes sense since the token should only be valid for a single room. @ladvoc can you confirm if the FFI needs a room, or if the url and token themselves are enough?

* @param source Track source (e.g. SOURCE_MICROPHONE).
* @param callback Function to invoke per audio frame.
*/
void registerOnAudioFrame(const std::string &participant_identity,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it register** the right word ?

I wonder what will happen if users register the same callback or multiple callbacks here ?
do we support those use cases ?

if not, can we make it a bit less ambiguous ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"register*" has been a pattern i have seen in the robotics space, but i see the confusion. I dont think we should support registering multiple callbacks for a single stream/track since IMO it adds necessary complexity, but i could be convinced otherwise.

I can rename to setOnAudioFrameCallback() to be more explicit?

* If a reader thread is active for this (identity, source), it is
* stopped and joined.
*/
void unregisterOnAudioFrame(const std::string &participant_identity,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like it should "clear" rather than unregister ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we agree on only one callback per stream/track, then 100% i agree this should be clearOnAudioFrameCallback()

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm. this is a bit worrying, are we saying that we will have multiple render threads if we want to render multiple tracks ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the *Reader() just handle getting audio/video frames -- it doesnt actually do any rendering/playback of said frames. These start*Reader() functions take in a callback which will get if a frame is received. Said callbacks will handle rendering/processing/etc.

* mic->mute();
* mic.reset(); // unpublishes
*/
class BridgeAudioTrack {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these BridgeAudioTrack, BridgeVideoTrack public interface to our developers ?

Note, from the code, these track implementations are not thread safe on its own, that says, if users want to get access to these bridge track, they are not protected

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a mutex to bridge_audio_track.h and bridge_video_track.h to ensure thread safety -- thanks you! This was a big miss!

released_ = true;

// Unpublish the track from the room
if (participant_ && publication_) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I read, the BridgeVideoTrack can outlive the LivekitBridge::disconnect(), like that can be destruct after calling livekitBridge::disconnect().
In that case, how does our code guarantee that the it will not call into the participant_ ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a good point i hadnt considered. I think we can handle it in a few ways:

  1. we store std::weak_ptrs to all the tracks in the livekit_bridge. When the livekit_bridge calls disconnect(), it steps through all tracks and calls release().
  2. the bridge could pass each track a atomic bool which gets set when the livekit_bridge calls disconnect(). Then the track checks said bool in its control flow.

I think we will likely want to do things to the tracks in the future, like maybe mute/unmute through a more generic livekit_bridge public function (like muteAllAudio()) which might justify using weak_ptrs being stored in the bridge, but im not sure what the correct approach is here. What is your though?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, should you add an assert to make sure room_ is nullptr here ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great point -- added assert here and when we std::make_unique<BridgeRoomDelegate>

{
std::lock_guard<std::mutex> lock(mutex_);

if (!connected_) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is wrong to return here.

For instance, if the connect() go thru the steps and fail at bool result = room_->Connect(url, token, options);

the connected_ is false, but you never call livekit::shutdown(); to clear up things

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch -- i replaced the return with:

      std::cerr << "[LiveKitBridge] Attempting to disconnect an already "
                   "disconnected bridge. Things may not disconnect properly.\n";

…s. livekit_bridge.cpp: disconnect() if cerr, but continue with disconnect
…ing_ flag to guard against calling connect() more than once
instead of calling stopThread() before creating, we do cleanup after calling start.
examples/robot.cpp for sends webcam/mic and sim video/audio feeds to the human participant. examples/human.cpp takes in human input to switch between real/sim streams
@stephen-derosa stephen-derosa force-pushed the feat/livekit_cpp_bridge.sdd branch from 1aa8e12 to 098e1f4 Compare February 18, 2026 01:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments